using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using FT.Architecture.Entities;
using FT.Architecture.Logging;
using FT.Architecture.NHibernate.Context;
using FT.Architecture.NHibernate.SessionManagement;
using FT.Architecture.Queries;
using FT.Architecture.Repositories;
using FT.Architecture.Utils.Reflection;
using NHibernate;
using NHibernate.Proxy;

namespace FT.Architecture.NHibernate.Repositories
{
    /// <summary>
    /// Implementation of a generic <see cref="FT.Architecture.NHibernate.Repositories"/> using <see cref="NHibernate"/>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TId"></typeparam>
    public class NHibernateGenericRepository<T, TId> : IGenericRepository<T, TId> where T : class, IGenericEntity<TId>
    {
        private readonly ISessionProviderService sessionProvider;
        private readonly IQueryExecutorService queryExecutor;
        private readonly ILogService logService;

        public NHibernateGenericRepository(ISessionProviderService sessionProvider, IQueryExecutorService queryExecutor, ILogService logService)
        {
            this.sessionProvider = sessionProvider;
            this.queryExecutor = queryExecutor;
            this.logService = logService;
        }

        /// <summary>
        /// Get an entity by its ID
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public T GetById(TId id)
        {
            logService.Repository.Info("Get Entity by Id " + GetTypeName<T>() + " " + id);

            return SessionProvider.GetSession().Get<T>(id);
        }

        /// <summary>
        /// Get an entity by its ID, and fetch the specified members as well
        /// </summary>
        /// <param name="id"></param>
        /// <param name="fetchExpressions"></param>
        /// <returns></returns>
        public T GetById(TId id, params Expression<Func<T, object>>[] fetchExpressions)
        {
            logService.Repository.Info(string.Format("Get Entity by Id with {2} fetch expressions {0} {1}", GetTypeName<T>(), id, fetchExpressions.Length));

            var queryBuilderEngine = new QueryBuilderEngine<T, TId>(fetchExpressions);
            string query = queryBuilderEngine.BuildQuery();

            query += " where _entity.id = :id";

            return SessionProvider.GetSession().CreateQuery(query)
                .SetParameter("id", id)
                .UniqueResult<T>();
        }

        /// <summary>
        /// Gets a random entity. This is helpful for test purposes.
        /// </summary>
        /// <returns></returns>
        public T GetRandom()
        {
            logService.Repository.Info("Get Random Entity " + GetTypeName<T>());

            var random = new Random((int)DateTime.Now.Ticks);
            long count = GetCount();
            return SessionProvider.GetSession().CreateQuery("from " + GetTypeName<T>())
                .SetFirstResult(random.Next(0, (int)count))
                .SetMaxResults(1)
                .UniqueResult<T>();
        }

        /// <summary>
        /// Returns the next available (not taken) ID
        /// </summary>
        /// <returns></returns>
        public TId GetNextId()
        {
            var results = (
                from TId count in
                    SessionProvider.GetSession().CreateQuery("select max(e.Id)+1 from " + GetTypeName<T>() + " e").List<long>()
                select count).Max();

            return results;
        }

        /// <summary>
        /// Get the total count of entities
        /// </summary>
        /// <returns></returns>
        public long GetCount()
        {
            logService.Repository.Info("Get Total count " + GetTypeName<T>());

            // We have to use Linq here because the NH query sometimes return more than one result (if the type span on multiple tables)
            var results = (
                from long count in
                    SessionProvider.GetSession().CreateQuery("select count(*) from " + GetTypeName<T>() + " e").List<long>()
                select count).Sum();

            return results;
        }

        /// <summary>
        /// Get an entity by one of its member value. This has to return only one result.
        /// </summary>
        /// <param name="member"></param>
        /// <param name="value"></param>
        /// <param name="fetchExpressions"></param>
        /// <returns></returns>
        public T GetByMember(Expression<Func<T, object>> member, object value, params Expression<Func<T, object>>[] fetchExpressions)
        {
            logService.Repository.Info(string.Format("Get By Member {0} {1} {2}", GetTypeName<T>(), member, value));

            return GetByMemberGeneric(
                new List<SearchExpression<T>>
                    {
                        new SearchExpression<T>(member, value),
                    },
                    fetchExpressions
                ).UniqueResult<T>();
        }

        /// <summary>
        /// Get an entity by two of its member value. This has to return only one result.
        /// </summary>
        /// <returns></returns>
        public T GetByMember(Expression<Func<T, object>> member1, object value1, Expression<Func<T, object>> member2, object value2, params Expression<Func<T, object>>[] fetchExpressions)
        {
            logService.Repository.Info(string.Format("Get By Member {0} {1} {2} {3} {4}", GetTypeName<T>(), member1, value1, member2, value2));

            return GetByMemberGeneric(
               new List<SearchExpression<T>>
                    {
                        new SearchExpression<T>(member1, value1),
                        new SearchExpression<T>(member2, value2),
                    },
                   fetchExpressions
               ).UniqueResult<T>();
        }

        /// <summary>
        /// Get an entity by three of its member value. This has to return only one result.
        /// </summary>
        /// <returns></returns>
        public T GetByMember(Expression<Func<T, object>> member1, object value1, Expression<Func<T, object>> member2, object value2, Expression<Func<T, object>> member3, object value3, params Expression<Func<T, object>>[] fetchExpressions)
        {
            logService.Repository.Info(string.Format("Get By Member {0} {1} {2} {3} {4} {5} {6}", GetTypeName<T>(), member1, value1, member2, value2, member3, value3));

            return GetByMemberGeneric(
               new List<SearchExpression<T>>
                    {
                        new SearchExpression<T>(member1, value1),
                        new SearchExpression<T>(member2, value2),
                        new SearchExpression<T>(member3, value3),
                    },
                   fetchExpressions
               ).UniqueResult<T>();
        }

        /// <summary>
        /// Get all entities of the specified type
        /// </summary>
        /// <returns></returns>
        public IList<T> GetAll()
        {
            logService.Repository.Info(string.Format("Get All Entities {0}", GetTypeName<T>()));

            return SessionProvider.GetSession().CreateQuery("from " + GetTypeName<T>())
                .List<T>();
        }

        /// <summary>
        /// Get all entities, and fetches the chosen members, using the provided delegate
        /// </summary>
        /// <param name="expressions"></param>
        /// <returns></returns>
        public IList<T> GetAll(params Expression<Func<T, object>>[] expressions)
        {
            logService.Repository.Info(string.Format("Get All Entities With Fetch {0}", GetTypeName<T>()));

            var queryBuilderEngine = new QueryBuilderEngine<T, TId>(expressions);
            string query = queryBuilderEngine.BuildQuery();

            return SessionProvider.GetSession().CreateQuery(query).List<T>();
        }

        /// <summary>
        /// Get all entities, and fetches the chosen members, using the provided delegate.
        /// You can even specify sub-items (i.e. item.SubItem.SubSubItem etc.).
        /// The results will be ordered by the specified member
        /// </summary>
        /// <param name="orderBy"></param>
        /// <param name="sortAscending"></param>
        /// <param name="fetchMembers"></param>
        /// <returns></returns>
        public IList<T> GetAll(Expression<Func<T, object>> orderBy, bool sortAscending, params Expression<Func<T, object>>[] fetchMembers)
        {
            logService.Repository.Info(string.Format("Get All Entities {0} ordered by {1} {2}", GetTypeName<T>(), orderBy, sortAscending ? "asc" : "desc"));

            var queryBuilderEngine = new QueryBuilderEngine<T, TId>(fetchMembers);
            string query = queryBuilderEngine.BuildQuery();

            string orderByExpression = Reflection.GetCombinedProperty(orderBy);
            string orderByDirection = sortAscending ? "asc" : "desc";

            query += " order by _entity." + orderByExpression + " " + orderByDirection;

            return SessionProvider.GetSession().CreateQuery(query).List<T>();
        }

        /// <summary>
        /// Get all entities, and fetches the chosen members, using the provided delegate.
        /// You can even specify sub-items (i.e. item.SubItem.SubSubItem etc.)
        /// The value can contain a '%' which indicates any character.
        /// </summary>
        /// <returns></returns>
        public IList<T> GetAllByMember(Expression<Func<T, object>> member, object value, params Expression<Func<T, object>>[] fetchMembers)
        {
            logService.Repository.Info(string.Format("Get All By Member {0} {1} {2}", GetTypeName<T>(), member, value));

            return GetByMemberGeneric(
                new List<SearchExpression<T>>
                    {
                        new SearchExpression<T>(member, value)
                    },
                    fetchMembers
                ).List<T>();
        }

        /// <summary>
        /// Get all entities, and fetches the chosen members, using the provided delegate.
        /// You can even specify sub-items (i.e. item.SubItem.SubSubItem etc.)
        /// This will be filtered by member1 and member2 values.
        /// </summary>
        /// <returns></returns>
        public IList<T> GetAllByMember(Expression<Func<T, object>> member1, object value1, Expression<Func<T, object>> member2, object value2, params Expression<Func<T, object>>[] fetchMembers)
        {
            logService.Repository.Info(string.Format("Get All By Member {0} {1} {2} {3} {4}", GetTypeName<T>(), member1, value1, member2, value2));

            return GetByMemberGeneric(
                new List<SearchExpression<T>>
                    {
                        new SearchExpression<T>(member1, value1),
                        new SearchExpression<T>(member2, value2)
                    },
                    fetchMembers
                ).List<T>();
        }

        /// <summary>
        /// Get all entities, and fetches the chosen members, using the provided delegate.
        /// You can even specify sub-items (i.e. item.SubItem.SubSubItem etc.)
        /// This will be filtered by member1, member2 and member3 values.
        /// </summary>
        /// <returns></returns>
        public IList<T> GetAllByMember(Expression<Func<T, object>> member1, object value1, Expression<Func<T, object>> member2, object value2, Expression<Func<T, object>> member3, object value3, params Expression<Func<T, object>>[] fetchMembers)
        {
            logService.Repository.Info(string.Format("Get By Member {0} {1} {2} {3} {4} {5} {6}", GetTypeName<T>(), member1, value1, member2, value2, member3, value3));

            return GetByMemberGeneric(
                new List<SearchExpression<T>>
                    {
                        new SearchExpression<T>(member1, value1),
                        new SearchExpression<T>(member2, value2),
                        new SearchExpression<T>(member3, value3)
                    },
                    fetchMembers
                ).List<T>();
        }

        /// <summary>
        /// Perform a search using a Query object
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public IEnumerable<T> FindBy(Query<T> query)
        {
            return queryExecutor.Execute(query);
        }

        /// <summary>
        /// Return the number of results a given query would return
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public long CountBy(Query<T> query)
        {
            return queryExecutor.Count(query);
        }

        /// <summary>
        /// Create a new query object
        /// </summary>
        /// <returns></returns>
        public Query<T> CreateQuery()
        {
            return new Query<T>();
        }

        /// <summary>
        /// Get Linq provider
        /// </summary>
        /// <returns></returns>
        public IQueryable<T> GetLinq()
        {
            logService.Repository.Info(string.Format("Get Linq Provider {0}", GetTypeName<T>()));

            throw new NotImplementedException("Not yet available with NH 3 it seems...");
        }

        /// <summary>
        /// Save an entity: Make it persistent, as opposed to transient
        /// </summary>
        /// <param name="entity"></param>
        public void Save(T entity)
        {
            logService.Repository.Info(string.Format("Save entity {0}, {1}", GetTypeName<T>(), entity));

            SessionProvider.GetSession().SaveOrUpdate(entity);
        }

        /// <summary>
        /// Batch saves a list of entities using a stateless session
        /// </summary>
        /// <param name="entities"></param>
        public void BulkSave(IEnumerable<T> entities)
        {
            logService.Repository.Info(string.Format("Batch saving {1} entities of type {0}", GetTypeName<T>(), entities.Count()));

            var statelessSession = SessionProvider.GetStatelessSession();
            foreach (var entity in entities)
            {
                statelessSession.Insert(entity);
            }
        }

        /// <summary>
        /// Delete an entity: Make it transient again
        /// </summary>
        /// <param name="entity"></param>
        public void Delete(T entity)
        {
            Delete(entity, true);
        }

        /// <summary>
        /// Delete an entity: Make it transient again.
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="resetId">if true, the Id will be reset to its default value</param>
        public void Delete(T entity, bool resetId)
        {
            Delete(new List<T> { entity }, resetId);
        }

        /// <summary>
        /// Delete a batch of entities. This will reset the entities Ids to their default values.
        /// </summary>
        /// <param name="entities"></param>
        public void Delete(params T[] entities)
        {
            Delete(true, entities);
        }

        /// <summary>
        /// Delete a batch of entities. This will reset the entities Ids to their default values.
        /// </summary>
        /// <param name="entities"></param>
        public void Delete(IEnumerable<T> entities)
        {
            Delete(entities, true);
        }

        /// <summary>
        /// Delete a batch of entities
        /// </summary>
        /// <param name="resetId">if true, the Id will be reset to its default value</param>
        /// <param name="entities"></param>
        public void Delete(bool resetId, params T[] entities)
        {
            Delete(new List<T>(entities), resetId);
        }

        /// <summary>
        /// Delete a batch of entities
        /// </summary>
        /// <param name="entities"></param>
        /// <param name="resetId">if true, the Id will be reset to its default value</param>
        public void Delete(IEnumerable<T> entities, bool resetId)
        {
            var session = SessionProvider.GetSession();

            foreach (var entity in entities)
            {
                logService.Repository.Info(string.Format("Delete entity {0}, {1}, {2}", GetTypeName<T>(), entity, entity.Id));

                // Delete the object from the database
                session.Delete(entity);
            }

            if (resetId)
            {
                // This is necessary if we reset the ids, otherwise NHibernate can't issue the proper delete statements when deleting by cascade.
                session.Flush();

                foreach (var entity in entities)
                {
                    // We make the entity transient again by nulling its ID
                    entity.Id = default(TId);
                }
            }
        }

        /// <summary>
        /// Un-proxy an object
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="proxiedEntity"></param>
        /// <returns></returns>
        public T UnProxy(T proxiedEntity)
        {
            // If the object is not a proxy, just cast it and returns it
            if (!(proxiedEntity is INHibernateProxy))
            {
                return proxiedEntity;
            }

            // Otherwise, use the NHibernate methods to get the implementation, and cast it
            var p = (INHibernateProxy)proxiedEntity;
            return (T)p.HibernateLazyInitializer.GetImplementation();
        }

        /// <summary>
        /// Get the concrete class of an object, from a proxied version
        /// </summary>
        /// <param name="proxiedEntity"></param>
        /// <returns></returns>
        public Type GetConcreteType(T proxiedEntity)
        {
            return NHibernateProxyHelper.GuessClass(proxiedEntity);
        }

        /// <summary>
        /// Detach an entity from its unit of work
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public T Detach(T entity)
        {
            return entity;
        }

        /// <summary>
        /// Reattach an entity to a current unit of work
        /// ALWAYS REPLACE YOUR REFERENCE TO THE ENTITY BY THE ONE RETURNED BY THIS METHOD
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="attachMode">Set the way the entity or the database will be updated if they differ</param>
        /// <returns></returns>
        public T Attach(T entity, AttachMode attachMode)
        {
            var session = SessionProvider.GetSession();

            switch (attachMode)
            {
                case AttachMode.UpdateDatabase:
                    // Merge the changes occured on the detached entity into the session
                    // The returned object might NOT be the same instance as the detached entity
                    var mergedEntity = (T)session.Merge(entity);
                    // Associate this merged entity into the session
                    session.Lock(mergedEntity, LockMode.None);

                    // That's why it's important to use the reference returned by the method:
                    // This reference might be different to the one given in parameter
                    return mergedEntity;

                case AttachMode.UpdateEntity:
                    // Associate the entity to the session
                    session.Lock(entity, LockMode.None);
                    // Refresh its state from the database
                    session.Refresh(entity, LockMode.None);
                    break;

                case AttachMode.NoUpdate:
                    //Just reassociate the entity
                    session.Lock(entity, LockMode.None);
                    break;

                default:
                    throw new ArgumentOutOfRangeException("attachMode");
            }

            return entity;
        }

        /// <summary>
        /// Gets information about the underlying storage
        /// </summary>
        public IGenericUnderlyingStorage<TId, T> UnderlyingStorage
        {
            get { return new NHibernateUnderlyingStorage<TId, T>(this); }
        }

        public ISessionProviderService SessionProvider
        {
            get { return sessionProvider; }
        }

        private static string GetTypeName<TType>()
        {
            return typeof(TType).Name;
        }

        private static string BuildWhereClause(IEnumerable<SearchExpression<T>> members)
        {
            string query = string.Empty;

            // Adds the actual clauses
            int i = 0;
            foreach (SearchExpression<T> expression in members)
            {
                string property = Reflection.GetCombinedProperty(expression.Member);
                query += string.Format("_entity.{0} like :val{1} and ", property, i);

                i++;
            }

            // Removes the last " and "
            if (i > 0)
            {
                query = query.Substring(0, query.Length - 5);
            }

            return query;
        }

        private static void SetParameters(IEnumerable<SearchExpression<T>> members, IQuery query)
        {
            int i = 0;
            foreach (SearchExpression<T> expression in members)
            {
                query.SetParameter("val" + i, expression.Value);
                i++;
            }
        }

        private IQuery GetByMemberGeneric(IEnumerable<SearchExpression<T>> expressions, params Expression<Func<T, object>>[] fetchMembers)
        {
            var queryBuilderEngine = new QueryBuilderEngine<T, TId>(fetchMembers);
            string queryText = queryBuilderEngine.BuildQuery();
            string whereClause = BuildWhereClause(expressions);

            queryText += " where " + whereClause;

            IQuery query = SessionProvider.GetSession().CreateQuery(queryText);
            SetParameters(expressions, query);

            return query;
        }
    }
}
